Willkommen zur Lerneinheit über die Java Persistence API. JPA ist ein zentrales Werkzeug in der Java-Welt, um Objekte dauerhaft in einer relationalen Datenbank zu speichern. Bisher war das Vorgehen in der Softwareentwicklung oft umständlich. Man modellierte zunächst ein UML-Klassenmodell auf Basis der objektorientierten Analyse und zusätzlich ein separates Entity-Relationship-Modell für die Datenbankstruktur. Änderungen im Modell erforderten dann mehrfachen Aufwand – im Code, im E-R-Modell und in der Datenbankstruktur selbst. Das führte zu Inkonsistenzen und viel manueller Nacharbeit. Die zentrale Frage lautet daher: Geht das nicht auch in einem Schritt? Genau hier setzt die Idee eines objektrelationalen Mappers – kurz ORM – an. Ziel ist es, nur noch das UML-Klassenmodell zu erstellen. Dieses wird dann direkt im Java-Code umgesetzt und zusätzlich mit sogenannten Annotationen versehen. Nach der Erstellung der UML-Klassendiagramme codet man diese Klassen. Diese Annotationen beschreiben, wie die Klassen und ihre Attribute in Tabellenstrukturen abgebildet werden sollen. Die E-R-Modellierung entfällt damit komplett, weil die UML-Modellierung ausreichend mächtig ist. Änderungen erfolgen nun direkt im Code, und die Tabellenstruktur wird dynamisch erzeugt. Ein ORM-Mapper ermöglicht es also, Objekte aus einem Java-Programm direkt in einer relationalen Datenbank abzulegen. Für das Programm erscheint die Datenbank dann als objektorientierte Datenbank. Das erleichtert das Programmieren, denn man arbeitet mit gewohnten Konzepten wie Klassen, Attributen und Beziehungen. Allerdings treffen hier zwei Welten aufeinander: Objektorientierte Programmiersprachen wie Java kapseln Daten und Verhalten in Objekten. Relationale Datenbanken hingegen speichern Daten in Tabellen, basierend auf der relationalen Algebra. Diese zwei Konzepte unterscheiden sich grundsätzlich. Dieser Widerspruch ist neunzehnhundertneunzig als "object-relational impedance mismatch" bekannt geworden – also als das Missverhältnis zwischen objektorientierter und relationaler Datenstruktur. Im einfachsten Fall werden bei der objekt-relationalen Abbildung mit JPA Java-Klassen direkt auf Tabellen abgebildet. Jedes Objekt entspricht dabei genau einer Tabellenzeile, und jedes Attribut einer Tabellenspalte. Die Identität eines Objekts entspricht dem Primärschlüssel der Tabelle. Hat ein Objekt eine Referenz auf ein anderes Objekt, so kann diese mit einer Fremdschlüssel-Primärschlüssel-Beziehung in der Datenbank dargestellt werden. Das bedeutet: Objekte und ihre Beziehungen lassen sich vollständig in relationalen Strukturen abbilden, ohne dass der Entwickler direkt mit SQL arbeiten muss. Die Java Persistence API ist eine standardisierte Schnittstelle in der Java-Plattform, die den Umgang mit Datenbankeinträgen deutlich vereinfacht. Sie löst das Problem der sogenannten objektrelationalen Abbildung, also der Herausforderung, wie Objekte aus dem Java-Code in Tabellen einer relationalen Datenbank überführt und dort dauerhaft gespeichert werden können. Genau dafür wurde JPA im Rahmen der Java Specification Request Nummer zweihundertzwanzig entwickelt und im Mai zweitausendsechs erstmals veröffentlicht. Eine zentrale Rolle spielt bei JPA das Konzept der sogenannten Persistence Entities. Das sind einfache Java-Klassen, sogenannte „Plain Old Java Objects“, die in der Regel jeweils einer Tabelle in der Datenbank entsprechen. Jede Instanz einer solchen Klasse bildet also eine Zeile in der Tabelle ab. Je nach Designansatz können diese Entities sehr schlicht gehalten sein – ähnlich einer Datenstruktur in C – oder sie können zusätzlich Geschäftslogik enthalten und als vollwertige Business-Objekte verwendet werden. Die Beziehungen zwischen den Entitäten, also zwischen den einzelnen Tabellen in der Datenbank, werden bei JPA über Metadaten beschrieben. Diese Metadaten können entweder direkt im Code als Annotationen eingefügt oder alternativ in einer externen XML-Datei abgelegt werden. Dadurch bleibt die Struktur flexibel und gut wartbar. Der Entwickler kann wählen, ob er seine Abbildung im Code selbst oder über eine Konfigurationsdatei steuern möchte. Für Datenbankabfragen bietet JPA eine eigene Abfragesprache, die Java Persistence Query Language. Diese Sprache ähnelt der bekannten Structured Query Language, bezieht sich aber auf Java-Objekte und nicht auf direkte Tabellennamen. Eine Abfrage fragt also nicht mehr eine Datenbanktabelle ab, sondern eine Entität – also eine Java-Klasse. Im Hintergrund übersetzt JPA diese JPQL-Abfragen zur Laufzeit in echtes SQL, das dann vom jeweiligen Datenbanksystem ausgeführt wird. Dadurch bleibt die Datenbank austauschbar, während die Java-Klassenstruktur erhalten bleibt. Neben der objektorientierten Abfragesprache JPQL erlaubt JPA auch den direkten Einsatz von SQL-Anfragen. Diese werden als sogenannte Native Queries bezeichnet. Man verliert ein Stück weit die Datenbankunabhängigkeit, denn der Entwickler muss sicherstellen, dass die jeweilige SQL-Abfrage mit dem Zielsystem kompatibel ist. EclipseLink ist ein Open-Source-Framework und ORM-Framework für die objekt-relationale Abbildung und Persistenz in Java. Es wurde von der Eclipse Foundation entwickelt und ermöglicht die Integration mit verschiedenen Datenquellen, darunter auch relationale Datenbanken, Webdienste und auch Object-XML-Mapping. EclipseLink ist eine der führenden Implementierungen für Jakarta Persistence und wird in vielen professionellen Java-Projekten eingesetzt. Um EclipseLink zusammen mit JPA in einem Java-Projekt zu verwenden, müssen zunächst bestimmte Bibliotheken eingebunden werden. Dafür werden die entsprechenden JAR-Dateien in einen Ressourcenordner kopiert und anschließend in den Build-Pfad des Projekts eingefügt. Da JPA intern auf die Java Database Connectivity zugreift, wird zusätzlich der passende Datenbank-Connector benötigt, zum Beispiel für unsere MariaDB. Ein zentraler Teil der Konfiguration erfolgt über eine XML-Datei. Dazu wird im Projektverzeichnis ein spezieller Ordner namens „Meta-Inf“ erstellt. In diesem Ordner legt man dann eine Datei mit dem Namen persistence.xml an. Diese Datei enthält alle notwendigen Informationen zur Datenbankverbindung und zur Definition der zu persistierenden Java-Klassen. In der persistence.xml wird unter anderem die Persistence Unit definiert. Der Name dieser Einheit muss exakt mit dem Namen übereinstimmen, der später im Java-Code verwendet wird. Es muss genau angegeben werden, welche Java-Klassen in der Datenbank gespeichert werden sollen. Diese Entitäten müssen einzeln aufgeführt werden. Für die Verbindung zur Datenbank wird der „Java Database Connectivity“-Treiber benötigt. Dieser Treiber stammt aus dem Java-Archiv des Connectors und wird in der Konfigurationsdatei angegeben. Ohne ihn kann keine Kommunikation mit dem Datenbankserver erfolgen. Die Verbindung zur Datenbank erfolgt lokal über den Standard-TCP-Port 3306, der von MariaDB genutzt wird. Wenn die angegebene Datenbank noch nicht existiert, wird sie automatisch neu angelegt. Voraussetzung dafür ist natürlich, dass der MariaDB-Benutzer entsprechende Rechte besitzt. Damit sich die Anwendung korrekt mit dem Datenbankserver verbinden kann, müssen in der XML-Datei ein Benutzername und ein Passwort eingetragen sein. Diese Anmeldedaten müssen auch tatsächlich auf dem Datenbank-Server existieren, damit die Authentifizierung gelingt. Falls in der Datenbank noch keine Tabellen vorhanden sind, werden diese automatisch erzeugt. Das spart dem Entwickler viel Arbeit, da keine manuelle Tabellenerstellung notwendig ist. Die Struktur wird direkt aus den Java-Klassen abgeleitet. Nun geht es darum, die eigentliche Fachlogik in Java umzusetzen. In unserem Beispielmodell haben wir verschiedene Arten von Kunden über eine Vererbung realisiert: Geschäftskunde, Privatkunde und VIP-Kunde. Jede dieser Klassen hat eigene Eigenschaften wie zum Beispiel Firmenname, Vorname oder Rabatt. Die abstrakte Kundenklasse bekommt als erste Klasse ihre Annotationen, um in die Datenbank abgebildet zu werden. Die ID und deren Auto-Inkrement werden definiert. Außerdem wird angegeben, wie die Vererbung umgesetzt werden soll. Jede konkrete Entitätsklasse in der Hierarchie erhält bei Table-per-Class eine eigene Tabelle, die alle geerbten und eigenen Felder enthält. Es existiert keine zentrale Tabelle für die Oberklasse. Dies sorgt jedoch für Redundanzen in den Feldern der Oberklasse. Bei Joined bekommt jede Klasse ihre eigene Tabelle. Alle Entitäten der Vererbungshierarchie werden mit Single-Class in einer einzigen Tabelle gespeichert. Eine zusätzliche Discriminator-Spalte zeigt dann die konkreten Subklassen. Es gibt jedoch viele Null-Spalteneinträge, weil jede Subklasse ihre eigenen Attribute in derselben Tabelle hat. Der Privatkunde ist ein spezieller Kunde. Dies kann JPA interpretieren und legt die entsprechenden Strukturen an, wie es in der Oberklasse definiert wurde. Dies gilt genauso für den Geschäftskunden. Und auch für den VIP-Kunden. Zusätzlich zu den Kundentypen kommt noch die Klasse Rechnung hinzu. Eine Rechnung hat eine Rechnungsnummer und ein Datum. Sie ist mit einem Kunden verknüpft und kann einen bestimmten Status sowie eine Zahlungsart besitzen. Diese Informationen werden zum Beispiel in Form von Enums abgebildet – also vordefinierte Werte wie „bezahlt“ oder „erste Mahnung“ für den Status oder „bar“, „Visa“ oder „PayPal“ für die Zahlungsart. Die Verknüpfung mit dem Kunden geschieht im E-R-Modell über eine 1-zu-N Beziehung. Und genau dies wird an der Referenz zum Kundenobjekt per Annotation mitgeteilt. Die Getter und Setter müssen alle öffentlich sein, damit JPA automatisch die Objekte aus der Datenbank einladen kann. Außerdem werden noch die Enums der Zahlungsart und des Rechnungsstatus gewöhnlich in Java programmiert. Jetzt ist es soweit: Wir wollen insgesamt vier Kunden und vier Rechnungen mit JPA dauerhaft speichern. Dazu benötigt man zuerst eine Entity-Manager-Fabrik (dies ist ein Entwurfsmuster), von dem dann ein Manager-Objekt erstellt wird. Über diesen erfolgt dann die Persistierung. Hier sehen wir die Ausgaben der Konsole beim Speichern. Das ist das SQL, welches auf die Datenbank abgesetzt wird. Es werden die korrekten Werte übergeben, etwa das Datum, der Status wie „bezahlt“ oder „offen“, die Zahlungsart wie „Visa“ oder „PayPal“ sowie die ID des Kunden. Das Speichern erfolgt vollständig automatisiert durch die JPA-Infrastruktur. Nun wird geprüft, wie die Daten in der Datenbank angekommen sind. Es wird dargestellt, welche Tabellen erzeugt wurden und welche Daten enthalten sind. Die gespeicherten Objekte erscheinen jetzt in relationaler Form. Man erkennt, wie die Kundeninformationen in einzelnen Tabellenzeilen gespeichert sind. Die Informationen aus Java wurden erfolgreich übernommen. Hier ist die Tabelle für den Geschäftskunden. Und hier für den Privatkunden. Und für den VIP-Kunden. Das ist die Tabelle der Rechnungen mit der Nummer, dem Datum, dem Status, der Zahlungsart und der Referenz auf den Kunden. Alle Strukturen wurden automatisch erstellt und dann mit den Daten aus den Objekten befüllt. Jetzt werden die gespeicherten Daten wieder in Java eingelesen. Dies geschieht wieder über die Manager-Fabrik, die einen Manager erzeugt. Das ResultSet als Ergebnis eines Queries ist nun sofort eine Liste von Objekten, die mit den Eigenschaften aus der Datenbank befüllt wurden. Als Ergebnis werden alle Rechnungen auf der Konsole ausgegeben. Das Schließen der offenen Verbindungen zur Datenbank sollte wie immer in einem try-finally Block geschehen, damit man bei einem Fehler keine verwaisten offenen Verbindungen hat. Die Ausgabe bestätigt, dass alle Informationen erfolgreich eingelesen wurden. Die einzelnen Kunden, inklusive ihrer Typen und IDs, sowie die zugehörigen Rechnungen mit Status und Zahlungsart, stehen der Anwendung nun wieder vollständig zur Verfügung.